#include "../../include/fileSystem/inode.h"
#include "../../include/fileSystem/fileSystem.h"
#include "../../include/process.h"
#include "../../include/kc.h"
#include "../../include/fileSystem/directory.h"

extern process_t processes[];	//lista de procesos
extern int currProc;			//proceso actual

#define MAX_OPENED_FILES 50

static int cantInode = 0;

//esta va a ser la tabla donde voy a poner los inodos de archivos abiertos,
//que voy a traer de disco
static inodeTableEntry inodeTable[MAX_OPENED_FILES];

static inodeTableEntry * getInodeByNum(int inodeNum);
static inodeTableEntry * consumePath(inodeTableEntry * base, char * filePath);
static inodeTableEntry * getBaseDir(char ** filePath);
static inodeTableEntry * getFreeInodeEntry(void);

inodeTableEntry *
getInode(char * filePathParam) {

	//hago esto para no modificar el str que me pasan
	char buff[MAX_PATH_LEN + 1], * filePath;
	if (strlen(filePathParam) >= MAX_PATH_LEN)
		return NULL;
	strcpy(buff, filePathParam);
	filePath = buff;

	//si me llega el filePath vacio, devuelvo una referencia al currDir
	if (*filePath == '\0')
		return copyInode(processes[currProc].currDir);

	//busco el directorio base a partir del cual voy a trabajar
	inodeTableEntry * base = getBaseDir(&filePath);

	inodeTableEntry * inode;

	//que me pidan por el directorio '/' es un caso especial. Si getBaseDir me
	//dejo '\0' en filePath, quiere decir que filePath solo tenia '/'
	if (*filePath == '\0') {
		inode = getInodeByNum(1);	//el inodo 1 siempre es el '/'
	}
	else {
		//consumo el path, devolviendo el inodo del archivo, o NULL si no lo encontro
		inode = consumePath(base, filePath);
	}

	return inode;
}

inodeTableEntry *
copyInode(inodeTableEntry * inode) {
	inode->timesOpened++;
	return inode;
}

void
putInode(inodeTableEntry * inode) {
	if (inode == NULL)
		return;

	inode->timesOpened--;
	if (inode->timesOpened == 0) {
		//libero el espacio en la tabla
		inode->inodeNum = 0;
		cantInode--;
	}
}

int
freeInode(inodeTableEntry * inode) {
	if (inode->timesOpened == 1) {
		int i;
		//libero los bloques directos
		for (i = 0; i < BLOCKS_PER_INODE - 1; i++) {
			if (inode->inode.blockNum[i] == 0)
				break;

			inode->inode.blockNum[i] = freeBlock(inode->inode.blockNum[i]);
		}

		//veo si tengo alocado el bloque indirecto. si lo tengo alocado, tengo que
		//liberar los bloques dentro de el
		if (i == (BLOCKS_PER_INODE - 1) && inode->inode.blockNum[i] != 0) {
			int * indirBlock;
			indirBlock = (int*)getBlock(inode->inode.blockNum[BLOCKS_PER_INODE-1]);

			//libero los bloques dentro del bloque indirecto
			for (i = 0; i < (BLOCK_SIZE / sizeof(int)); i++) {
				if (indirBlock[i] == 0)
					break;

				freeBlock(indirBlock[i]);
			}

			putBlock(inode->inode.blockNum[BLOCKS_PER_INODE-1], (char*)indirBlock);
			//libero el bloque indirecto
			inode->inode.blockNum[BLOCKS_PER_INODE-1] =
				freeBlock(inode->inode.blockNum[BLOCKS_PER_INODE-1]);
		}

		freeInodeByNum(inode->inodeNum);
		putInode(inode);
		return 1;
	}

	return -1;
}

int
doUnlink(const char * pathParam, int opNum) {
	char name[FILE_NAME_LEN+1], path[MAX_PATH_LEN];

	strcpy(path, pathParam);

	//obtengo el nombre del archivo o directorio a unlinkear
	getNameFromPath(name, path);

	if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
		return -1;

	inodeTableEntry * parentInode;

	//obtengo el inodo del directorio donde se encuentra el objetivo
	if ( (parentInode = getInode(path)) == NULL ) {
		return -1;
	}

	inodeTableEntry * inode;
	//obtengo el inodo que voy a borrar
	if ( (inode = consumePath(parentInode, name)) == NULL ) {
		putInode(parentInode);
		return -1;
	}

	//voy a dejar borrar archivo que solo tengan una referencia: la anterior
	if (inode->timesOpened != 1) {
		putInode(parentInode);
		putInode(inode);
		return -1;
	}

	//si estoy borrando un directorio, veo que este vacio
	if (opNum == DIR_UNLINK) {
		//si no es un directorio o el tamaño no coincide con
		//dos entradas ("." y ".."), salgo
		if ( !(inode->inode.mode & DIR_BIT)
				|| inode->inode.fileSize != sizeof(dirEntry)*2) {
			putInode(parentInode);
			putInode(inode);
			return -1;
		}
	}
	else {
		//si no es un archivo, salgo
		if ( !(inode->inode.mode & FILE_BIT) ) {
			putInode(parentInode);
			putInode(inode);
			return -1;
		}
	}

	//busco el bloque del directorio padre
	dirEntry * dir = (dirEntry*)getBlock(parentInode->inode.blockNum[0]);

	//borro la entrada del directorio correspondiente
	rmDirEntry(dir, name, &parentInode->inode.fileSize);

	//actualizo el inodo del directorio y su bloque
	writeBlock(parentInode->inode.blockNum[0], (char *)dir);
	putBlock(parentInode->inode.blockNum[0], (char *)dir);
	writeInode(parentInode->inodeNum, &parentInode->inode);
	putInode(parentInode);

	return freeInode(inode);
}

inodeTableEntry *
createInode(char * filePathParam, int * parentInode, int flags) {
	char name[FILE_NAME_LEN+1];

	//hago esto para no modificar el str que me pasan
	char buff[MAX_PATH_LEN + 1], * filePath;
	if (strlen(filePathParam) >= MAX_PATH_LEN)
		return NULL;
	strcpy(buff, filePathParam);
	filePath = buff;

	//por default seteo el padre del inodo que quiero crear en -1
	if (parentInode != NULL)
		*parentInode = -1;

	//saco el nombre del archivo o directorio que quiero crear del filePath
	getNameFromPath(name, filePath);

	//busco el directorio base a partir del cual voy a trabajar
	inodeTableEntry * base = getBaseDir(&filePath);

	//voy a buscar el inodo del directorio en el que se va a encontrar
	//el directorio o archivo que quiero crear
	inodeTableEntry * dirInode = consumePath(base, filePath);

	//si el path era incorrecto, salgo
	if (dirInode == NULL)
		return NULL;

	//chequeo que dirInode sea un inodo apuntando a un directorio
	if (!(dirInode->inode.mode & DIR_BIT)) {
		putInode(dirInode);
		return NULL;
	}

	//busco el bloque del directorio
	dirEntry * dir = (dirEntry*)getBlock(dirInode->inode.blockNum[0]);

	int inodeNum;
	//me fijo si el archivo o directorio ya existe
	inodeNum = searchDir(dir, dirInode->inode.fileSize, name);

	inodeTableEntry * inode = NULL;
	//si no es -1, quiere decir que el inodo ya existia
	//si es -1 y O_CREAT esta encendido, tengo que crear un nuevo inodo
	if (inodeNum != -1) {
		//si los flags tienen O_EXCL | O_CREAT encendido, no abro el inodo
		if (!( (flags & O_EXCL) && (flags & O_CREAT) ))
			inode = getInodeByNum(inodeNum);
	}
	else if(flags & O_CREAT){
		inode = getFreeInodeEntry();
		//si encontre una entrada libre, trato de alocar en disco
		if (inode != NULL) {
			inode->inodeNum = allocInode(&inode->inode);
			//si pude alocar el inodo en el disco, lo marco presente y agrego
			//el numero de inodo al directorio que lo contiene
			if (inode->inodeNum != -1) {
				inode->timesOpened = 1;
				//al agregar la entrada tengo que actualizar el tamaño del dir
				//si parentInode es NULL, estoy creando un archivo, sino un dir
				if (parentInode != NULL)
					addDirEntry(dir, name, inode->inodeNum, &dirInode->inode.fileSize, 'd');
				else
					addDirEntry(dir, name, inode->inodeNum, &dirInode->inode.fileSize, 'f');
				//actualizo el inodo del directorio en disco
				writeInode(dirInode->inodeNum, &dirInode->inode);
				//actualizo el bloque del directorio en disco
				writeBlock(dirInode->inode.blockNum[0], (char *)dir);
			}
			else {
				inode = NULL;
			}
		}
	}

	if (parentInode != NULL)
		*parentInode = dirInode->inodeNum;
	//devuelvo el bloque e inodo del subdirectorio que ya no necesito
	putBlock(dirInode->inode.blockNum[0], (char *)dir);
	putInode(dirInode);

	return inode;
}

static inodeTableEntry *
consumePath(inodeTableEntry * base, char * filePath) {
	static char name[FILE_NAME_LEN+1];
	int inodeNum;

	//avanza sobre filePath hasta un '/' o un '\0'
	filePath = parsePath(name, filePath);

	//busco el bloque del directorio
	dirEntry * dir = (dirEntry*)getBlock(base->inode.blockNum[0]);

	//busco el nombre del subdirectorio o el archivo en dir
	//el segundo parametro es la cantidad de elementos del directorio
	inodeNum = searchDir(dir, base->inode.fileSize, name);

	//devuelvo el bloque del subdirectorio a disco
	putBlock(base->inode.blockNum[0], (char *)dir);

	//si no encontro el nombre, o no se encontro el inodo de inodeNum, salgo
	if (inodeNum == -1 || (base = getInodeByNum(inodeNum)) == NULL)
		return NULL;

	//si me quede sin filePath, quiere decir que encontre el inodo, que quedo
	//en base, asi que lo devuelvo
	if (*filePath == '\0') {
		return base;
	}

	//chequeo que base sea un inodo apuntando a un directorio
	if (!(base->inode.mode & DIR_BIT))
		return NULL;

	inodeTableEntry * inode;
	//si aun no lo encontre, llamo nuevamente a consumePath
	inode = consumePath(base, filePath);
	//devuelvo el inodo del subdirectorio que ya no necesito
	putInode(base);

	return inode;
}

//funcion auxiliar que trata de ver si el inodo se encuentra en la tabla.
//de ser asi, devuelve una referencia al inodo. si no se encuentra, lo trae
//a la tabla y devuelve una referencia a el.
static inodeTableEntry *
getInodeByNum(int inodeNum) {
	int i, firstFreeInode = -1;
	//primero veo si el inodo esta en memoria (en la tabla)
	for (i = 0; i < MAX_OPENED_FILES; i++) {
		if (inodeTable[i].inodeNum == inodeNum) {
			//si estaba, aumento la cantidad de veces que fue abierto
			inodeTable[i].timesOpened++;
			return &inodeTable[i];
		}
		//si encuentro un inodo vacio, lo guardo por si tengo que traer el
		//inodo del fileSystem
		if (inodeTable[i].timesOpened == 0 && firstFreeInode == -1)
			firstFreeInode = i;
	}

	inode_t aux;
	//si no estaba en memoria, lo voy a tener que ir a buscar al fileSystem
	//si no tengo espacio en la tabla o fallo el read, devuelvo NULL
	if (firstFreeInode == -1 || readInode(inodeNum, &aux) == -1)
		return NULL;

	//lo guardo en la tabla
	memcpy(&inodeTable[firstFreeInode].inode, &aux, sizeof(inode_t));
	inodeTable[firstFreeInode].inodeNum = inodeNum;
	inodeTable[firstFreeInode].openedForWrite = 0;
	inodeTable[firstFreeInode].timesOpened = 1;

	cantInode++;

	return &inodeTable[firstFreeInode];
}

//funcion auxiliar que dependiendo si el filePath pasado es relativo o
//absoluto, devuelve el currentDir o el rootDir. En el segundo caso,
//ademas, avanza el filePath hasta la primer letra distinta de '/'
static inodeTableEntry *
getBaseDir(char ** filePath) {
	if (**filePath == '/') {
		//consumo todas las '/'
		while(**filePath == '/')
			(*filePath)++;
		return processes[currProc].rootDir;
	}

	return processes[currProc].currDir;
}

static inodeTableEntry *
getFreeInodeEntry() {
	int i;
	for (i = 0; i < MAX_OPENED_FILES; i++) {
		if (inodeTable[i].timesOpened == 0)
			return &inodeTable[i];
	}
	return NULL;
}

int
findCurrDirPath(char * name, int size) {

	inodeTableEntry * base = processes[currProc].currDir;

	//si ya estoy en '/', devuelvo eso
	if (base->inodeNum == 1) {
		strcpy(name, "/");
		return 1;
	}

	//busco el bloque del directorio inicial
	dirEntry * dir = (dirEntry *)getBlock(base->inode.blockNum[0]);

	//guardo como el proximo numero de inodo el que esta en ".."
	int nextInodeNum = dir[1].inode;

	//devuelvo el bloque del directorio inicial
	putBlock(base->inode.blockNum[0], (char *)dir);

	//salvo mi numero como el numero previo
	int prevInodeNum = base->inodeNum;

	//en sizeAux va a estar el tamaño que me va quedando para guardar el
	//current path en name
	int sizeAux = size - 1;

	inodeTableEntry * currInode;

	do {
		//obtengo el inodo del directorio apuntado por nextInodeNum
		currInode = getInodeByNum(nextInodeNum);

		//busco el bloque del directorio actual
		dir = (dirEntry *)getBlock(currInode->inode.blockNum[0]);

		int i, cant = currInode->inode.fileSize/sizeof(dirEntry);
		//como el nombre del directorio previo se encuentra en el directorio
		//actual, ciclo hasta encontrar el nombre y lo agrego a name
		for (i = 0; i < cant; i++) {
			if (dir[i].inode == prevInodeNum) {
				int len = strlen(dir[i].name);
				sizeAux -= (len + 1);  //+1 para agregar la '/' al final
				if (sizeAux >= 0) {
					name[sizeAux] = '/';
					memcpy(name + sizeAux + 1, dir[i].name, len);
				}
				break;
			}
		}
		//actualizo el inodo anterior y el posterior de forma acorde
		prevInodeNum = nextInodeNum;
		nextInodeNum = dir[1].inode;

		//devuelvo el bloque del directorio actual, y el inodo
		putBlock(currInode->inode.blockNum[0], (char *)dir);
		putInode(currInode);

		//si me quede sin tamaño, salgo con error
		if (sizeAux < 0)
			return -1;

	}while(prevInodeNum != 1); //1 es el inodo del dir root

	memmove(name, name + sizeAux, size - sizeAux);
	name[size - sizeAux] = '\0';

	return 1;
}

